﻿using System;
using System.Collections;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using BizElements.Core;
using BizElements.BusinessLayer;
using BizElements.Web;

namespace BizElements.Web
{
    /// <summary>Utility class for common business object binding operations.</summary>
    public static class BindingsUtil
    {
        /// <summary>A sign appended to field labels which indicates that a field is not nullable.</summary>
        public const string RequiredFieldIndicator = "*";

        #region NewBindingContext.

        /// <summary>
        /// Creates a new form binding context.
        /// </summary>
        /// <param name="mainErrorLabel">Label to which all unhandled errors are written.</param>
        /// <returns>BusinessObjectBindingContext with default messages for parsing errors.</returns>
        public static BusinessObjectBindingContext NewBindingContext(Label mainErrorLabel)
        {
            return new BusinessObjectBindingContext(UserSession.Formatter, mainErrorLabel,
                UiMessages.DefaultParseError, UiMessages.IntegerParseError, UiMessages.NumberParseError, UiMessages.DateTimeParseError);
        }

        #endregion

        #region ValidateBusinessObject.

        /// <summary>
        /// Validates the business object and displays encountered validation errors.
        /// </summary>
        /// <param name="bizObject">Business object which is to be validated.</param>
        /// <param name="bindings">Form FieldBindings which specify how validation errors are handled.</param>
        /// <returns><b>true</b> if object is valid and succesfully saved; <b>false</b> otherwise.</returns>
        public static bool ValidateBusinessObject(ISelfValidatingObject bizObject, IValueObjectBindingContext bindings)
        {
            BusinessRuleCollection brokenRules = bizObject.Validate(ValidationLevel.AllBusinessRules);
            bool isValid = !brokenRules.HasBrokenRules;
            if (!isValid)
                bindings.DisplayErrors(brokenRules);

            return isValid;
        }

        #endregion

        #region PopulateAndValidateBusinessObject.

        /// <summary>
        /// Populates and validates the business object and displays encountered validation errors.
        /// </summary>
        /// <param name="bizObject">Business object which is to be populated and validated.</param>
        /// <param name="bindings">Form FieldBindings which specify how validation errors are handled.</param>
        /// <returns><b>true</b> if object is valid and succesfully saved; <b>false</b> otherwise.</returns>
        public static bool PopulateAndValidateBusinessObject(ISelfValidatingObject bizObject, IValueObjectBindingContext bindings)
        {
            BusinessRuleCollection parseErrors = bindings.PopulateBusinessObject(bizObject, true);
            if (parseErrors.HasBrokenRules)
                return false;

            BusinessRuleCollection brokenRules = bizObject.Validate(ValidationLevel.AllBusinessRules);
            if (brokenRules.HasBrokenRules)
            {
                bindings.DisplayErrors(brokenRules);
                return false;
            }

            return true;
        }

        #endregion

        #region NewGridAdapter.

        /// <summary>
        /// Creates a new instance of the <see cref="BusinessObjectsGridAdapter"/> class.
        /// </summary>
        /// <param name="grid">Grid used to edit underying business objects.</param>
        /// <param name="businessObjects">A list of <see cref="ISelfValidatingObject"/> or <see cref="IBusinessObject"/> instances used as a data-source for the grid.</param>
        /// <returns>BusinessObjectsGridAdapter with default messages for parsing errors.</returns>
        public static BusinessObjectsGridAdapter NewGridAdapter(BoundDataGrid grid, IList businessObjects)
        {
            BusinessObjectsGridAdapter adapter = new BusinessObjectsGridAdapter(grid, businessObjects);
            adapter.DefaultParseError = UiMessages.DefaultParseError;
            adapter.IntegerParseError = UiMessages.IntegerParseError;
            adapter.NumberParseError = UiMessages.NumberParseError;
            adapter.DatetimeParseError = UiMessages.DateTimeParseError;

            return adapter;
        }

        #endregion

        #region BindField.

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="dbField">Database field.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, IDbColumn dbField, ICaptionProvider captions, IBoundControl fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            BindableField bizField = new BindableField(dbField.PropertyName, dbField.DataType, captions, satelliteFields);
            var fieldBinding = bindings.BindField(bizField, fieldControl);
            if (captionLabel != null)
            {
                fieldBinding.CaptionLabel = captionLabel;
                if (string.IsNullOrEmpty(captionLabel.Text))
                    captionLabel.Text = (dbField.IsNullable) ? captions.GetFieldCaption(dbField.PropertyName) : captions.GetFieldCaption(dbField.PropertyName) + RequiredFieldIndicator;
            }

            return bizField;
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="dbField">Database field.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, IDbColumn dbField, ICaptionProvider captions, BoundTextBox fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            int? maxLenIfText = TypeUtil.IsText(dbField.DataType) ? (int?)dbField.MaxLengthIfText : null;
            SetTextBoxConstraints(fieldControl, dbField.DataType, maxLenIfText);
            return BindField(bindings, dbField, captions, (IBoundControl)fieldControl, captionLabel, satelliteFields);
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="dbField">Database field.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, IDbColumn dbField, bool isNullable, ICaptionProvider captions, IBoundControl fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            BindableField bizField = new BindableField(dbField.PropertyName, dbField.DataType, captions, satelliteFields);
            var fieldBinding = bindings.BindField(bizField, fieldControl);
            if (captionLabel != null)
            {
                fieldBinding.CaptionLabel = captionLabel;
                if (string.IsNullOrEmpty(captionLabel.Text))
                    captionLabel.Text = (dbField.IsNullable) ? captions.GetFieldCaption(dbField.PropertyName) : captions.GetFieldCaption(dbField.PropertyName) + RequiredFieldIndicator;
            }

            return bizField;
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="dbField">Database field.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, IDbColumn dbField, bool isNullable, ICaptionProvider captions, BoundTextBox fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            int? maxLenIfText = TypeUtil.IsText(dbField.DataType) ? (int?)dbField.MaxLengthIfText : null;
            SetTextBoxConstraints(fieldControl, dbField.DataType, maxLenIfText);
            return BindField(bindings, dbField, isNullable, captions, (IBoundControl)fieldControl, captionLabel, satelliteFields);
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="property">Public property.</param>
        /// <param name="dataType">Type of data contained in the property.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, string property, Type dataType, bool isNullable, ICaptionProvider captions, IBoundControl fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            BindableField bizField = new BindableField(property, dataType, captions, satelliteFields);
            var fieldBinding = bindings.BindField(bizField, fieldControl);
            if (captionLabel != null)
            {
                fieldBinding.CaptionLabel = captionLabel;
                if (string.IsNullOrEmpty(captionLabel.Text))
                    captionLabel.Text = (isNullable) ? captions.GetFieldCaption(property) : captions.GetFieldCaption(property) + RequiredFieldIndicator;
            }

            return bizField;
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="property">Public property.</param>
        /// <param name="dataType">Type of data contained in the property.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, string property, Type dataType, bool isNullable, ICaptionProvider captions, BoundTextBox fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            SetTextBoxConstraints(fieldControl, dataType, null);
            return BindField(bindings, property, dataType, isNullable, captions, (IBoundControl)fieldControl, captionLabel, satelliteFields);
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="property">Public property.</param>
        /// <param name="dataType">Type of data contained in the property.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="caption">The caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, string property, Type dataType, bool isNullable, string caption, IBoundControl fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            BindableField bizField = new BindableField(property, caption, dataType, satelliteFields);
            var fieldBinding = bindings.BindField(bizField, fieldControl);
            if (captionLabel != null)
            {
                fieldBinding.CaptionLabel = captionLabel;
                if (string.IsNullOrEmpty(captionLabel.Text))
                    captionLabel.Text = (isNullable) ? caption : caption + RequiredFieldIndicator;
            }

            return bizField;
        }

        /// <summary>
        /// Binds property to specified edit/display and caption controls.
        /// </summary>
        /// <param name="bindings">Form binding context.</param>
        /// <param name="property">Public property.</param>
        /// <param name="dataType">Type of data contained in the property.</param>
        /// <param name="isNullable">Whether the value is required or not.</param>
        /// <param name="caption">The caption of the specified property.</param>
        /// <param name="fieldControl">Edit/display control.</param>
        /// <param name="captionLabel">Caption control.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bound field.</returns>
        public static IBindableField BindField(IValueObjectBindingContext bindings, string property, Type dataType, bool isNullable, string caption, BoundTextBox fieldControl, ITextControl captionLabel, params string[] satelliteFields)
        {
            SetTextBoxConstraints(fieldControl, dataType, null);
            return BindField(bindings, property, dataType, isNullable, caption, (IBoundControl)fieldControl, captionLabel, satelliteFields);
        }

        #endregion

        #region NewBindableField.

        /// <summary>
        /// Creates new bindable field.
        /// </summary>
        /// <param name="dbField">Database field.</param>
        /// <param name="captions">An object which contains the caption of the specified property.</param>
        /// <param name="satelliteFields">Optional satellite fields.</param>
        /// <returns>Bindable field.</returns>
        public static IBindableField NewBindableField(IDbColumn dbField, ICaptionProvider captions, params string[] satelliteFields)
        {
            return new BindableField(dbField.PropertyName, dbField.DataType, captions, satelliteFields);
        }

        #endregion

        #region PopulateBusinessObject.

        /// <summary>
        /// Populates business object properties with the values stored in bound controls.
        /// Automatically displays encountered parse errors.
        /// </summary>
        /// <param name="bizObject">Business object that is receiving the new values.</param>
        /// <param name="bindings">FieldBinding context that links controls to business object properties.</param>
        /// <returns><b>true if all values where succesfuly parsed and business object was populated</b>; <b>false</b> otherwise.</returns>
        /// <remarks>If an errors is encountered, none of the bound properties will receive a new value.</remarks>
        public static bool PopulateBusinessObject(ISelfValidatingObject bizObject, IValueObjectBindingContext bindings)
        {
            BusinessRuleCollection errors = bindings.PopulateBusinessObject(bizObject, true);
            return errors.AllRulesAreObeyed;
        }

        #endregion

        #region Textual control and template constraints - private for now.

        /// <summary>
        /// Automatically tries to set MaxLength and AllowedChars properties if not already set.
        /// </summary>
        /// <param name="txtbox">BoundTextBox control to which constraints are to be applied.</param>
        /// <param name="dataType">Bindable field data type.</param>
        /// <param name="maxLengthIfText">Max length if bindable field is textual.</param>
        private static void SetTextBoxConstraints(BoundTextBox txtbox, Type dataType, int? maxLengthIfText)
        {
            bool alreadySet = (txtbox.MaxLength > 0) || !string.IsNullOrEmpty(txtbox.AllowedChars);
            if (alreadySet)
                return;

            bool isShortText = maxLengthIfText.HasValue && (maxLengthIfText.Value < 1000);
            if (TypeUtil.IsText(dataType) && isShortText)
            {
                txtbox.MaxLength = maxLengthIfText.Value;
            }
            else if (TypeUtil.IsInteger(dataType))
            {
                txtbox.MaxLength = 10;
                txtbox.AllowedChars = "-0123456789";
            }
            else if (TypeUtil.IsFloatingPointNumber(dataType))
            {
                txtbox.AllowedChars = "-0123456789,.";
            }
        }

        /// <summary>
        /// Automatically tries to set MaxLength and AllowedChars properties if not already set.
        /// </summary>
        /// <param name="textTemplate">BoundTextBoxTemplate grid template to which constraints are to be applied.</param>
        /// <param name="dataType">Bindable field data type.</param>
        /// <param name="maxLengthIfText">Max length if bindable field is textual.</param>
        private static void SetTextBoxConstraints(BoundTextBoxTemplate textTemplate, Type dataType, int? maxLengthIfText)
        {
            bool alreadySet = (textTemplate.MaxLength > 0) || !string.IsNullOrEmpty(textTemplate.AllowedChars);
            if (alreadySet)
                return;

            bool isShortText = maxLengthIfText.HasValue && (maxLengthIfText.Value < 1000);
            if (TypeUtil.IsText(dataType) && isShortText)
            {
                textTemplate.MaxLength = maxLengthIfText.Value;
            }
            else if (TypeUtil.IsInteger(dataType))
            {
                textTemplate.MaxLength = 10;
                textTemplate.AllowedChars = "-0123456789";
            }
            else if (TypeUtil.IsFloatingPointNumber(dataType))
            {
                textTemplate.AllowedChars = "-0123456789,.";
            }
        }

        #endregion

        #region RemoveRequiredFieldIndicator.

        /// <summary>Removes required field indicator (eg. asterisk) from the end of the given label.</summary>
        public static void RemoveRequiredFieldIndicator(ITextControl label)
        {
            label.Text = TextUtil.RemoveSuffix(label.Text, RequiredFieldIndicator);
        }

        /// <summary>Removes required field indicator (eg. asterisk) from the end of the given labels.</summary>
        public static void RemoveRequiredFieldIndicators(params ITextControl[] labels)
        {
            foreach (ITextControl lbl in labels)
                RemoveRequiredFieldIndicator(lbl);
        }

        #endregion

        #region BindLookupToGrid.

        /// <summary>Binds a foreign-key child field to grid and creates a column with BoundLookup controls.</summary>
        /// <param name="grid">BoundDataGrid that displays and edits the collection.</param>
        /// <param name="field">Key field mapped to entity property which is bound to grid. The value of the field is hidden in UI.</param>
        /// <param name="captions">Object that provides caption/title for the <b>field</b>.</param>
        /// <param name="parentDisplayFieldPath">Path to parent object's display property. The values of this property is displayed in UI, ie. Label and BoundLookup controls. Eg. "ObjectTypeParent.Description"</param>
        /// <param name="uiSearch">UI component on the same form as grid which prepares search engine and search controls.</param>
        /// <param name="engineBuilder">Object that creates a search engine that searches parent objects.</param>
        /// <param name="idColumn">Column fetched by search engine that contains the unique key for parent object, ie. the value which is written to child entity's property mapped to foreign key.</param>
        /// <param name="displayColumn">Column fetched by search engine that contains value displayed in UI.</param>
        public static void BindLookupToGrid(BoundDataGrid grid, IDbColumn field, ICaptionProvider captions, string parentDisplayFieldPath, SearchProviderBuilder uiSearch, ISearchEngineBuilder engineBuilder, string idColumn, string displayColumn)
        {
            var bindableField = BindingsUtil.NewBindableField(field, captions, parentDisplayFieldPath);
            var lookupTemplate = new BoundLookupTemplate(/*popup or iframe*/ true);
            var labelTemplate = new BoundLabelTemplate() { DisplayFields = new string[] { parentDisplayFieldPath } };
            grid.BindField(bindableField, lookupTemplate, labelTemplate);

            uiSearch.EngineBuilder = engineBuilder;
            uiSearch.RegisterForClient(UserSession.UiCulture, lookupTemplate, /*popup or iframe*/ true, new string[] { idColumn, displayColumn });
        }

        #endregion

        #region BindAutoCompleteToGrid.

        /// <summary>Binds a foreign-key child field to grid and creates a column with BoundAutoComplete controls.</summary>
        /// <param name="grid">BoundDataGrid that displays and edits the collection.</param>
        /// <param name="field">Key field mapped to entity property which is bound to grid. The value of the field is hidden in UI.</param>
        /// <param name="captions">Object that provides caption/title for the <b>field</b>.</param>
        /// <param name="parentDisplayFieldPath">Path to parent object's display property. The values of this property is displayed in UI, ie. Label and BoundAutomComplete controls. Eg. "ObjectTypeParent.Description"</param>
        /// <param name="uiSearch">UI component on the same form as grid which prepares search engine and search controls.</param>
        /// <param name="engineBuilder">Object that creates a search engine that searches parent objects.</param>
        /// <param name="keyColumn">Column fetched by search engine that contains the unique key for parent object, ie. the value which is written to child entity's property mapped to foreign key.</param>
        /// <param name="displayColumn">Column fetched by search engine that contains value displayed in UI.</param>
        public static void BindAutoCompleteToGrid(BoundDataGrid grid, IDbColumn field, ICaptionProvider captions, string parentDisplayFieldPath,
            SearchProviderBuilder uiSearch, ISearchEngineBuilder engineBuilder, string keyColumn, string displayColumn)
        {
            var bindableField = BindingsUtil.NewBindableField(field, captions, parentDisplayFieldPath);
            var lookupTemplate = new BoundAutoCompleteTemplate();
            var labelTemplate = new BoundLabelTemplate() { DisplayFields = new string[] { parentDisplayFieldPath } };
            grid.BindField(bindableField, lookupTemplate, labelTemplate);

            uiSearch.EngineBuilder = engineBuilder;
            uiSearch.RegisterForClient(UserSession.UiCulture, lookupTemplate, false, new string[] { keyColumn, displayColumn });
        }

        #endregion
    }
}
